package cz.byteworks.endownload;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.LogFactory;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.ErrorHandler;

import com.gargoylesoftware.htmlunit.AjaxController;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.IncorrectnessListener;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ScriptException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.HTMLParserListener;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlDivision;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlListItem;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput;
import com.gargoylesoftware.htmlunit.html.HtmlTable;
import com.gargoylesoftware.htmlunit.html.HtmlTableDataCell;
import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
import com.gargoylesoftware.htmlunit.html.HtmlUnorderedList;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptErrorListener;

import cz.byteworks.exportino.ICredentials;
import cz.byteworks.exportino.IEventListener;
import cz.byteworks.exportino.IRecordLoader;
import cz.byteworks.exportino.IRecordProcessor;
import cz.byteworks.exportino.LoadException;
import cz.byteworks.exportino.NoRecordsException;

public class ActivityLoader implements IRecordLoader {
	
	private Properties properties;
	final WebClient webClient = new WebClient();
	private static Format format = Format.GPX;
	
	public static final String PROPERTY_KEY_FORMAT = "format";
	private String baseURLPattern = "http://www.endomondo.com/";
	private String loginURL = "https://www.endomondo.com/access";
	private String workoutsURL = "http://www.endomondo.com/workouts/";
	private String workoutListURL = "http://www.endomondo.com/workouts/list";
	private List<String> workoutIds = new ArrayList<String>();
	private boolean isPrepared = false;
	
	public ActivityLoader(Properties properties) {
		this.properties = properties;
		if (this.properties.getProperty(PROPERTY_KEY_FORMAT) != null && this.properties.getProperty(PROPERTY_KEY_FORMAT).equalsIgnoreCase("tcx")) {
			format = Format.TCX;
		}
	}

	@Override
	public void prepare(ICredentials credentials, IEventListener eventListener) throws LoadException, NoRecordsException {
		isPrepared = false;
		try {
    		disableDebugOutputs();
    		webClient.setAjaxController(new AjaxController(){
				private static final long serialVersionUID = 1L;

				@Override
    		    public boolean processSynchron(HtmlPage page,
    		    		WebRequest request, boolean async) {
    		    	return true;
    		    }
    		});
	    	
    		login(credentials);
    		
    		String earliestWorkoutId = getEarliestWorkoutId();
    		if (earliestWorkoutId == null) {
    			throw new NoRecordsException();
    		}
	        
	        HtmlPage workoutsPage = webClient.getPage(workoutsURL);
	        
	        DomNodeList<HtmlElement> anchors; 
	        ListIterator<HtmlElement> listIterator; 
	        HtmlTable calendar;
	        
	        boolean earliestReached = false;
	        do {
	        	calendar = (HtmlTable) workoutsPage.getDocumentElement().getElementsByAttribute("table", "class", "calendar").get(0);
	        	anchors = calendar.getElementsByTagName("a");
	        	listIterator = anchors.listIterator(anchors.size());
		        while(listIterator.hasPrevious()) {
		        	HtmlAnchor anchor = (HtmlAnchor) listIterator.previous();
		        	if (anchor.getHrefAttribute() == null || anchor.getHrefAttribute().indexOf(workoutsURL) < 0) {
		        		continue;
		        	}
		        	String wId = extractWorkoutIdFromURL(anchor.getHrefAttribute());
		        	workoutIds.add(wId); //TODO count workouts
		        	System.out.println("Added workout "+wId); 
		        }
		        if (workoutIds.contains(earliestWorkoutId)) {
		        	earliestReached = true;
		        }
		        else {
		        	workoutsPage = getPreviousMonth(workoutsPage);
		        }
	        } while (!earliestReached);
    	}
    	catch (Exception e) {
    		e.printStackTrace();
    		throw new LoadException("Unable to load workouts: " + e.getMessage());
    	}
		isPrepared = true;
	}
	
	@Override
	public void process(IRecordProcessor processor, IEventListener eventListener) throws LoadException {
		if (!isPrepared) {
			throw new LoadException("Loader is not prepared");
		}
		for (String workoutId : workoutIds) {
			try {
				eventListener.onEvent(IEventListener.Type.INFO, "Processing workout " + workoutId, null);
				HtmlPage workoutPage = (HtmlPage) webClient.getPage(workoutsURL + workoutId);
				// TODO process only enabled workouts
				if (processor != null) {
	        		processor.processRecord(createActivity(workoutPage));
	        	}
				eventListener.onEvent(IEventListener.Type.INFO, "Workout " + workoutId + " done", null);
			} catch (Exception e) {
				eventListener.onEvent(IEventListener.Type.ERROR, e.getMessage(), e);
			}
		}
	}
    
    public static void main(String[] args) {
		Properties props = new Properties();
		ActivityLoader activityLoader = new ActivityLoader(props);
		Credentials credentials = new Credentials();
		credentials.setUsername("petr.koula@gmail.com");
		credentials.setPassword("everlasting");
		try {
			IRecordProcessor processor = new ActivityProcessor("c:/out");
			IEventListener eventListener = new EventListener();
			activityLoader.prepare(credentials, eventListener);
			activityLoader.process(processor, eventListener);
		} catch (LoadException e) {
			e.printStackTrace();
		} catch (NoRecordsException e) {
			e.printStackTrace();
		}
	}
    
    private HtmlPage getPreviousMonth(HtmlPage workoutsPage) throws FailingHttpStatusCodeException, MalformedURLException, IOException {
		HtmlDivision monthsDiv = (HtmlDivision) workoutsPage.getDocumentElement().getElementsByAttribute("div", "class", "month-nav").get(0);
		HtmlUnorderedList ul = (HtmlUnorderedList) monthsDiv.getByXPath("ul").get(0);
		String prevLink = null;
		for (Object el : ul.getByXPath("li")) {
			HtmlListItem li = (HtmlListItem) el;
			if (li.getAttribute("class") != null && li.getAttribute("class").equals("current")) {	
				return webClient.getPage(baseURLPattern + prevLink);
			}
			else {
				HtmlAnchor a = (HtmlAnchor) li.getByXPath("a").get(0);
				prevLink = a.getHrefAttribute();
			}
		}
		return null;
	}

	private String getEarliestWorkoutId() throws FailingHttpStatusCodeException, MalformedURLException, IOException {
		HtmlPage historyPage = webClient.getPage(workoutListURL);
		historyPage = getEarliestHistoryPage(historyPage);
		DomElement lastRow = getLastElementByTagNameAndClass(historyPage, "tr", "row");
		if (lastRow == null) {
			return null;
		}
		HtmlTableDataCell cell = (HtmlTableDataCell) lastRow.getByXPath("td[2]").get(0);
		HtmlPage workoutPage = cell.click();
		return extractWorkoutIdFromURL(workoutPage.getUrl().toString());
	}

	private HtmlPage getEarliestHistoryPage(HtmlPage historyPage) throws IOException {
		DomElement nextSpan = getFirstElementByTagNameAndClass(historyPage, "span", "nav-back");
		if (nextSpan == null || nextSpan.getByXPath("a").isEmpty()) {
			return historyPage;
		}
		DomElement lastPageLinkSpan = getLastElementByTagNameAndClass(historyPage, "span", "pages");
		if (!lastPageLinkSpan.getByXPath("a").isEmpty()) {
			HtmlAnchor anchor = (HtmlAnchor) lastPageLinkSpan.getByXPath("a").get(0);
			return getEarliestHistoryPage((HtmlPage)webClient.getPage(baseURLPattern + extractLinkFromHref(anchor.getHrefAttribute())));
		}
		else {
			return historyPage;
		}
	}
	
	private List<HtmlElement> getElementsByTagNameAndClass(HtmlPage page, String tagName, String className) {
		return page.getDocumentElement().getElementsByAttribute(tagName, "class", className);
	}
	
	private HtmlElement getFirstElement(List<HtmlElement> list) {
		if (list != null && !list.isEmpty()) {
			return list.get(0);
		}
		return null;
	}
	
	private HtmlElement getLastElement(List<HtmlElement> list) {
		if (list != null && !list.isEmpty()) {
			return list.get(list.size() - 1);
		}
		return null;
	}
	
	private HtmlElement getFirstElementByTagNameAndClass(HtmlPage page, String tagName, String className) {
		return getFirstElement(getElementsByTagNameAndClass(page, tagName, className));
	}
	
	private HtmlElement getLastElementByTagNameAndClass(HtmlPage page, String tagName, String className) {
		return getLastElement(getElementsByTagNameAndClass(page, tagName, className));
	}

	private void login(ICredentials credentials) throws FailingHttpStatusCodeException, MalformedURLException, IOException, LoadException {
		HtmlPage loginPage = webClient.getPage(loginURL);
    	HtmlForm form = loginPage.getFormByName("signInForm");

        HtmlAnchor link = loginPage.getAnchorByHref("#");
        if (link.getAttribute("class") == null || !link.getAttribute("class").equals("next button")) {
        	throw new LoadException("Could not parse Endomondo login page");
        }
        HtmlTextInput email = form.getInputByName("email");
        HtmlPasswordInput password = form.getInputByName("password");
        
        email.setValueAttribute(credentials.getUsername());
        password.setValueAttribute(credentials.getPassword());

        link.click();
	}
    
    private Activity createActivity(HtmlPage workoutPage) throws FailingHttpStatusCodeException, MalformedURLException, IOException {
		Activity workout = new Activity();
		
		String workoutId = extractWorkoutIdFromURL(workoutPage.getUrl().toString());
    	HtmlAnchor exportAnchor = getExportLink(workoutPage);
    	String exportLink = extractLink(exportAnchor.getOnClickAttribute(), false);
    	Page downloadPopup = webClient.getPage(baseURLPattern + exportLink);
    	String downloadLink = extractLink(downloadPopup.getWebResponse().getContentAsString(), true);
    	Page exportedData = webClient.getPage(baseURLPattern + downloadLink);
    	workout.setData(exportedData.getWebResponse().getContentAsString());
    	workout.setId(workoutId);
    	workout.setType(format == Format.GPX ? "gpx" : "tcx");
    	
    	return workout;
	}
    
    private HtmlAnchor getExportLink(HtmlPage page) {
    	List<HtmlAnchor> anchors = page.getAnchors();
    	for (HtmlAnchor htmlAnchor : anchors) {
			if (htmlAnchor.getAttribute("class") != null && htmlAnchor.getAttribute("class").equals("enabled button export")) {
				return htmlAnchor;
			}
		}
    	return null;
    }
    
    private String extractLink(String attribute, boolean forDownload) {
    	String exportFormat = format == Format.GPX ? "Gpx" : "Tcx";
    	Pattern pattern = Pattern.compile(forDownload ? "<a href=\"(.+export"+exportFormat+"Link[^\"]+)\"" : "wcall=wicketAjaxGet\\('\\.\\./(.*?)'");
    	Matcher matcher = pattern.matcher(attribute);
    	if (matcher.find())
    	{
    	    return matcher.group(1);
    	}
    	return null;
    }
    
    private String extractLinkFromHref(String attribute) {
    	Pattern pattern = Pattern.compile("[\\.\\./]+(.*?)$");
    	Matcher matcher = pattern.matcher(attribute);
    	if (matcher.find())
    	{
    	    return matcher.group(1);
    	}
    	return null;
    }
    
    private String extractWorkoutIdFromURL(String url) {
    	Pattern pattern = Pattern.compile("workouts/(.*?)(?:/|$)");
    	Matcher matcher = pattern.matcher(url);
    	if (matcher.find())
    	{
    	    return matcher.group(1);
    	}
    	return null;
    }
    
    private void disableDebugOutputs() {
    	LogFactory.getFactory().setAttribute("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.NoOpLog");
    	webClient.getOptions().setThrowExceptionOnScriptError(false);
		webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
        java.util.logging.Logger.getLogger("com.gargoylesoftware.htmlunit").setLevel(Level.OFF); 
        java.util.logging.Logger.getLogger("org.apache.commons.httpclient").setLevel(Level.OFF);

        webClient.getOptions().setCssEnabled(false);

        webClient.setIncorrectnessListener(new IncorrectnessListener() {

            @Override
            public void notify(String arg0, Object arg1) {
                // TODO Auto-generated method stub

            }
        });
        webClient.setCssErrorHandler(new ErrorHandler() {
			
			@Override
			public void warning(CSSParseException arg0) throws CSSException {
			}
			
			@Override
			public void fatalError(CSSParseException arg0) throws CSSException {
			}
			
			@Override
			public void error(CSSParseException arg0) throws CSSException {
			}
		});
        webClient.setJavaScriptErrorListener(new JavaScriptErrorListener() {

			@Override
			public void loadScriptError(HtmlPage paramHtmlPage, URL paramURL,
					Exception paramException) {
			}

			@Override
			public void malformedScriptURL(HtmlPage paramHtmlPage,
					String paramString,
					MalformedURLException paramMalformedURLException) {
			}

			@Override
			public void scriptException(HtmlPage paramHtmlPage,
					ScriptException paramScriptException) {
			}

			@Override
			public void timeoutError(HtmlPage paramHtmlPage, long paramLong1,
					long paramLong2) {
			}

           
        });
        webClient.setHTMLParserListener(new HTMLParserListener() {

			@Override
			public void error(String paramString1, URL paramURL,
					String paramString2, int paramInt1, int paramInt2,
					String paramString3) {
			}

			@Override
			public void warning(String paramString1, URL paramURL,
					String paramString2, int paramInt1, int paramInt2,
					String paramString3) {
			}

           
        });
    }
    
    
    enum Format {
    	GPX,
    	TCX
    }
}
